﻿// -----------------------------------------------------------------------
// <copyright file="SessionManager.cs" company="FastNET Services">
//     Copyright (c) 2008-2009. All Rights Reserved.
// </copyright>
// <license>
//     GPLv2
// </license>
// <warranty>
//     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
//     BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
//     DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </warranty>
// -----------------------------------------------------------------------
#region Usings

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.ServiceModel;
using System.Threading;
using GateShift.Servers.GameServer.LoggingSystem;
using GateShift.Servers.GameServer.Managers.Connection;
using GateShift.Servers.GameServer.Managers.Session.Contracts;
using GateShift.Services.SDK.Services;
using GateShift.Services.SDK.Services.Events;
using GateShift.Services.SDK.Services.HostIntegration;
using GateShift.Services.SDK.Services.Hosts;
using GateShift.Services.SDK.Threading;

#endregion

namespace GateShift.Servers.GameServer.Managers.Session
{
    /// <summary>
    /// Management class for all client sessions
    /// </summary>
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    internal class SessionManager : ISessionManager, ISessionHost, ICommandLineInterfaceHandler, ISessionManagerSync
    {
        #region Variables

        private readonly ClientSessionCollection _clientSessions;
        private readonly ThreadedDictionary<Guid, Guid> _connectionReferences;
        private readonly ThreadedDictionary<Guid, Guid> _sessionReferences;
        private readonly ThreadedDictionary<byte, IClientDataHandler> _packetHandlers;
        private readonly List<Guid> _expiredSessions;
        private Thread _sessionManagerThread;
        private byte? _authenticationServiceID;

        #endregion

        #region CTor/DTor

        /// <summary>
        /// Initializes a new instance of the SessionManager class
        /// </summary>
        public SessionManager()
        {
            _connectionReferences = new ThreadedDictionary<Guid, Guid>();
            _sessionReferences = new ThreadedDictionary<Guid, Guid>();
            _packetHandlers = new ThreadedDictionary<byte, IClientDataHandler>();
            _clientSessions = new ClientSessionCollection();
            _expiredSessions = new List<Guid>();
        }

        #endregion

        #region Events

        /// <summary>
        /// Event for a session being disconnected
        /// </summary>
        public event EventHandler<ClientSessionEventArgs> SessionDisconnected;

        /// <summary>
        /// Event for a session being connected
        /// </summary>
        public event EventHandler<ClientSessionEventArgs> SessionConnected;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the reference to the connection manager
        /// </summary>
        public IConnectionManager ConnectionManager { get; set; }

        /// <summary>
        /// Gets a value indicating whether or not the manager status shows it is running
        /// </summary>
        public bool IsAlive
        {
            get
            {
                return _sessionManagerThread != null && _sessionManagerThread.IsAlive;
            }
        }

        #endregion

        #region ICommandLineInterfaceHandler Members

        /// <summary>
        /// Handles commands destined for the Session Manager
        /// </summary>
        /// <param name="arguments">List of arguments parsed from the command handler</param>
        /// <returns>bool based on whether or not the command was successfully handled</returns>
        bool ICommandLineInterfaceHandler.HandleCommand(List<string> arguments)
        {
            /*
                Session ByID <SessionID> Property <PropertyName>
                Session ByID <SessionID> Time
                Session ByID <SessionID> Logout
                Session ByID <SessionID> Disconnect
                Session All Disconnect
                Session All Logout
             */

            return false;
        }

        #endregion

        #region IManager Implementation

        /// <summary>
        /// Starts the manager background thread
        /// </summary>
        public void Start()
        {
            if (IsAlive)
            {
                throw new InvalidOperationException("Stop must be called before restarting");
            }

            _sessionManagerThread = new Thread(SessionManagerThread)
                                        {
                                            Name = "Session Manager",
                                            Priority = ThreadPriority.BelowNormal
                                        };

            Log.AddEntry("Starting Manager", "SessionManager", LogPriorities.Informative, TraceEventType.Start, (int)CoreLogCategories.ServiceManager);

            _sessionManagerThread.Start();
        }

        /// <summary>
        /// Stops the manager background thread
        /// </summary>
        public void Stop()
        {
            if (IsAlive)
            {
                Log.AddEntry("Stopping Manager", "SessionManager", LogPriorities.Informative, TraceEventType.Stop, (int)CoreLogCategories.ServiceManager);
                _sessionManagerThread.Abort();
                _sessionManagerThread.Join(10000);
            }

            _sessionManagerThread = null;
        }

        /// <summary>
        /// Initializes all manager resources
        /// </summary>
        public void Initialize()
        {
            Log.AddEntry("Initializing Manager", "SessionManager", LogPriorities.Informative, TraceEventType.Verbose, (int)CoreLogCategories.ServiceManager);
        }

        #endregion

        #region ISessionHost Members

        /// <summary>
        /// Disconnects the specified client by ID
        /// </summary>
        /// <param name="sessionID">Session ID to disconnect</param>
        public void DisconnectClient(Guid sessionID)
        {
            Guid clientID;

            // Make sure we have a client to relate to the session ID
            if (!_sessionReferences.TryGetValue(sessionID, out clientID))
            {
                ISessionManagerSync sync = ConnectionManager.OpenServiceChannel<ISessionManagerSync>();
                sync.RequestDisconnect(new RequestSessionDataContract() { SessionID = sessionID });
                ConnectionManager.CloseServiceChannel(sync);
                return;
            }

            ConnectionManager.Disconnect(clientID);
        }

        /// <summary>
        /// Sends a set of data to a client
        /// </summary>
        /// <param name="sessionID">Session ID to send data to</param>
        /// <param name="serviceID">Client Service ID to talk to</param>
        /// <param name="serviceCode">Client Service Code to send</param>
        /// <param name="data">Serialized Data for the client</param>
        /// <param name="encrypt">Whether or not to encrypt the data to the client</param>
        public void SendClientData(Guid sessionID, byte serviceID, byte serviceCode, byte[] data, bool encrypt)
        {
            Guid clientID;

            // Make sure we have a client to relate to the session ID
            if (!_sessionReferences.TryGetValue(sessionID, out clientID))
            {
                return;
            }

            ConnectionManager.SendData(clientID, new ClientPacket { PacketData = data, ServiceCode = serviceCode, ServiceID = serviceID, WasEncrypted = encrypt });
        }

        /// <summary>
        /// Sends a set of data to a list of specific clients
        /// </summary>
        /// <param name="sessionIDs">Session ID to send data to</param>
        /// <param name="serviceID">Client Service ID to talk to</param>
        /// <param name="serviceCode">Client Service Code to send</param>
        /// <param name="data">Serialized Data for the client</param>
        /// <param name="encrypt">Whether or not to encrypt the data to the client</param>
        public void SendClientData(List<Guid> sessionIDs, byte serviceID, byte serviceCode, byte[] data, bool encrypt)
        {
            foreach (Guid item in sessionIDs)
            {
                SendClientData(item, serviceID, serviceCode, data, encrypt);
            }
        }

        /// <summary>
        /// Sends a set of data to all clients
        /// </summary>
        /// <param name="serviceID">Client Service ID to talk to</param>
        /// <param name="serviceCode">Client Service Code to send</param>
        /// <param name="data">Serialized Data for the client</param>
        /// <param name="encrypt">Whether or not to encrypt the data to the client</param>
        public void SendClientData(byte serviceID, byte serviceCode, byte[] data, bool encrypt)
        {
            foreach (Guid item in _clientSessions.Keys)
            {
                SendClientData(item, serviceID, serviceCode, data, encrypt);
            }
        }

        /// <summary>
        /// Gets a reference to a specific client
        /// </summary>
        /// <param name="sessionID">Session ID to retrieve</param>
        /// <returns>IClientSession with the requested session or NULL if no session was found.</returns>
        public IClientSession GetSession(Guid sessionID)
        {
            return SessionExists(sessionID) ? _clientSessions[sessionID] : null;
        }

        /// <summary>
        /// Gets a list of all client sessions
        /// </summary>
        /// <returns>List of all client sessions</returns>
        public IList<IClientSession> GetSession()
        {
            IList<IClientSession> items = new List<IClientSession>();

            foreach (ClientSession item in _clientSessions.Values)
            {
                items.Add(item);
            }

            return items;
        }

        /// <summary>
        /// Checks to see whether or not the specified session exists
        /// </summary>
        /// <param name="sessionID">Session ID to check</param>
        /// <returns>bool based on whether or not the specified session exists</returns>
        public bool SessionExists(Guid sessionID)
        {
            return _clientSessions.ContainsKey(sessionID);
        }

        /// <summary>
        /// Registers an IClientDataHandler for recieving data from clients
        /// </summary>
        /// <param name="serviceID">ServiceID to register the handler for</param>
        /// <param name="handler">IClientDataHandler to add to the handler list</param>
        /// <exception cref="InvalidOperationException">Thrown if the ServiceID already exists</exception>
        public void RegisterClientDataHandler(byte serviceID, IClientDataHandler handler)
        {
            if (_packetHandlers.ContainsKey(serviceID))
            {
                throw new InvalidOperationException("Service ID Already Exists");
            }

            if (handler is IAuthenticationClientDataHandler)
            {
                _authenticationServiceID = serviceID;
            }

            _packetHandlers.Add(serviceID, handler);
        }

        #endregion

        #region Implementation of ISessionManagerSync

        /// <summary>
        /// Tells the session manager that a client has connected elsewhere
        /// </summary>
        /// <param name="request">RequestSessionDataContract with information about the call</param>
        void ISessionManagerSync.ClientConnected(RequestSessionDataContract request)
        {
            if (_clientSessions.ContainsKey(request.SessionID))
            {
                OnSessionConnected(_clientSessions[request.SessionID]);
            }
        }

        /// <summary>
        /// Tells the session manager that a client has disconnected elsewhere
        /// </summary>
        /// <param name="request">RequestSessionDataContract with information about the call</param>
        void ISessionManagerSync.ClientDisconnected(RequestSessionDataContract request)
        {
            if (_clientSessions.ContainsKey(request.SessionID))
            {
                OnSessionDisconnected(_clientSessions[request.SessionID]);
            }
        }

        /// <summary>
        /// Tells the session manager to disconnect the requested client
        /// </summary>
        /// <param name="request">RequestSessionDataContract with information about the call</param>
        void ISessionManagerSync.RequestDisconnect(RequestSessionDataContract request)
        {
            DisconnectClient(request.SessionID);
        }

        #endregion

        #region Event Raisers

        /// <summary>
        /// Raises the event for a session being connected
        /// </summary>
        /// <param name="session">Session being affected</param>
        protected void OnSessionConnected(ClientSession session)
        {
            if (SessionConnected != null)
            {
                SessionConnected(this, new ClientSessionEventArgs(session));
            }
        }

        /// <summary>
        /// Raises the event for a session being disconnected
        /// </summary>
        /// <param name="session">Session being affected</param>
        protected void OnSessionDisconnected(ClientSession session)
        {
            if (SessionDisconnected != null)
            {
                SessionDisconnected(this, new ClientSessionEventArgs(session));
            }
        }

        #endregion

        #region Manager Functions

        /// <summary>
        /// Background manager thread for session manager
        /// </summary>
        private void SessionManagerThread()
        {
            // Catch thread aborting
            try
            {
                Log.AddEntry("Thread Started", "Session Manager Thread", LogPriorities.Verbose, TraceEventType.Verbose, (int)CoreLogCategories.ConsoleManager);

                // Register ConnectionManager Events
                if (ConnectionManager != null)
                {
                    ConnectionManager.ClientConnected += ConnectionManager_ClientConnected;
                    ConnectionManager.ClientDisconnected += ConnectionManager_ClientDisconnected;
                    ConnectionManager.ClientPacketRecieved += ConnectionManager_ClientPacketRecieved;

                    ConnectionManager.AddServiceHost("SessionSync", this);
                }

                while (true)
                {
                    // Handle expired sessions
                    CheckExpiredSessions();
                    Thread.Sleep(60000);
                }
            }
            catch (ThreadAbortException)
            {
                Log.AddEntry("Thread Stopping", "Session Manager Thread", LogPriorities.Verbose, TraceEventType.Verbose, (int)CoreLogCategories.ConsoleManager);

                // Unregister ConnectionManager Events
                if (ConnectionManager != null)
                {
                    ConnectionManager.ClientConnected -= ConnectionManager_ClientConnected;
                    ConnectionManager.ClientDisconnected -= ConnectionManager_ClientDisconnected;
                    ConnectionManager.ClientPacketRecieved -= ConnectionManager_ClientPacketRecieved;

                    ConnectionManager.RemoveServiceHost("SessionSync");
                }
            }
        }

        #endregion

        #region Session Check Methods

        /// <summary>
        /// Looks at all expired sessions and notifys the authentication service
        /// </summary>
        private void CheckExpiredSessions()
        {
            // Get all expired logged in sessions we have not already dealt with
            List<ClientSession> sessions = (from session in _clientSessions.Values
                                            where session.IsTokenExpired &&
                                                !_expiredSessions.Contains(session.SessionID) && session.IsLoggedIn
                                            select session).ToList();

            // Notify the authentication service that the client has expired
            foreach (ClientSession session in sessions)
            {
                ((IAuthenticationClientDataHandler)_packetHandlers[_authenticationServiceID.Value]).HandleExpiredToken(session);
            }

            // Store that we handled the expired sessions
            _expiredSessions.AddRange(from session in sessions select session.SessionID);
        }

        #endregion

        #region ClientSession Events

        /// <summary>
        /// Event Handler for a client session renewing their token
        /// </summary>
        /// <param name="sender">ClientSession calling this event</param>
        /// <param name="e">Empty EventArgs</param>
        private void ClientSession_TokenRenewed(object sender, EventArgs e)
        {
            _expiredSessions.Remove(((ClientSession)sender).SessionID);
        }

        #endregion

        #region ConnectionManager Events

        /// <summary>
        /// Event handler for when a client sends data to the connection manager
        /// </summary>
        /// <param name="sender">ConnectionManager calling this event</param>
        /// <param name="e">ClientPacketEventArgs with information about the client and packet</param>
        private void ConnectionManager_ClientPacketRecieved(object sender, ClientPacketEventArgs e)
        {
            Guid sessionID;

            // Make sure we have a session to relate to the connection ID
            if (!_connectionReferences.TryGetValue(e.ClientID, out sessionID))
            {
                return;
            }

            // Get our session to work with
            ClientSession session = _clientSessions[sessionID];

            // If there is a login service code defined and the user is not logged in, only allow login packets
            if (!session.IsLoggedIn && _authenticationServiceID.HasValue && (e.Packet.ServiceID != _authenticationServiceID))
            {
                // Send an empty packet to login service to say we rejected what the client sent
                ((IAuthenticationClientDataHandler)_packetHandlers[_authenticationServiceID.Value]).HandleUnauthenticatedData(session);
            }
            else
            {
                // Send to service
                if (_packetHandlers.ContainsKey(e.Packet.ServiceID))
                {
                    _packetHandlers[e.Packet.ServiceID].HandleClientData(session, e.Packet.ServiceCode, e.Packet.PacketData, e.Packet.WasEncrypted);
                }
            }
        }

        /// <summary>
        /// Event handler for when a client disconnects from the connection manager
        /// </summary>
        /// <param name="sender">ConnectionManager calling this event</param>
        /// <param name="e">ClientEventArgs with information about the client</param>
        private void ConnectionManager_ClientDisconnected(object sender, ClientEventArgs e)
        {
            Guid oldSessionID;

            // Make sure we have a session to relate to the connection ID
            if (!_connectionReferences.TryGetValue(e.ClientID, out oldSessionID))
            {
                return;
            }

            _clientSessions[oldSessionID].TokenRenewed -= ClientSession_TokenRenewed;

            ISessionManagerSync sync = ConnectionManager.OpenServiceChannel<ISessionManagerSync>();
            sync.ClientDisconnected(new RequestSessionDataContract() { SessionID = oldSessionID });
            ConnectionManager.CloseServiceChannel(sync);

            OnSessionDisconnected(_clientSessions[oldSessionID]);

            _clientSessions.RemoveSafe(oldSessionID);
            _sessionReferences.RemoveSafe(oldSessionID);
            _connectionReferences.RemoveSafe(e.ClientID);
            _expiredSessions.Remove(oldSessionID);
        }

        /// <summary>
        /// Event handler for when a client connects to the connection manager
        /// </summary>
        /// <param name="sender">ConnectionManager calling this event</param>
        /// <param name="e">ClientEventArgs with information about the client</param>
        private void ConnectionManager_ClientConnected(object sender, ClientEventArgs e)
        {
            ClientSession newSession = new ClientSession(this);
            _clientSessions.Add(newSession.SessionID, newSession);
            _sessionReferences.Add(newSession.SessionID, e.ClientID);
            _connectionReferences.Add(e.ClientID, newSession.SessionID);

            newSession.TokenRenewed += ClientSession_TokenRenewed;

            ISessionManagerSync sync = ConnectionManager.OpenServiceChannel<ISessionManagerSync>();
            sync.ClientDisconnected(new RequestSessionDataContract() { SessionID = newSession.SessionID });
            ConnectionManager.CloseServiceChannel(sync);

            OnSessionConnected(newSession);
        }

        #endregion
    }
}